package com.davfx.httpnio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

final class HttpConnecter {

	private static final int MAX_REDIRECT = 5;
	
	private static final class RecyclingData {
		private final Connection connection;
		public RecyclingData(Connection connection) {
			this.connection = connection;
		}
	}
	
	private final Worker worker;
	private final Connecter connecter;
	private final Map<String, List<RecyclingData>> recycling = new HashMap<String, List<RecyclingData>>();
	
	public HttpConnecter(Worker worker) {
		this.worker = worker;
		connecter = new Connecter(worker);
	}
	
	public void close() {
		Map<String, List<RecyclingData>> r;
		synchronized (recycling) {
			r = new HashMap<String, List<RecyclingData>>(recycling);
			recycling.clear();
		}
		for (List<RecyclingData> l : r.values()) {
			for (RecyclingData d : l) {
				d.connection.close();
			}
		}
	}
	
	public HttpConnection connect(String host, int port, String method, String path, Map<String, String> headers, HttpReadConnectHandler handler) {
		final String recyclingKey = host + String.valueOf(Http.PORT_SEPARATOR) + String.valueOf(port);
		RecyclingData recyclingData = null;
		synchronized (recycling) {
			List<RecyclingData> recyclingDataList = recycling.get(recyclingKey);
			if (recyclingDataList != null) {
				for (Iterator<RecyclingData> i = recyclingDataList.iterator(); i.hasNext();) {
					recyclingData = i.next();
					i.remove();
				}
				if (recyclingDataList.isEmpty()) {
					recycling.remove(recyclingKey);
				}
			}
		}
		
		Connection connection;
		if (recyclingData != null) {
			connection = recyclingData.connection;
		} else {
			connection = connecter.connect(host, port);
		}
		
		InnerConnection innerConnection = new InnerConnection(worker);
		HttpConnection c = connect(host, port, method, path, headers, recyclingKey, connection, new InnerRedirectReadHandler(MAX_REDIRECT, innerConnection, host, port, method, handler));
		innerConnection.set(c);
		return innerConnection;
	}
	
	private final class InnerRedirectReadHandler implements HttpReadConnectHandler {
		private final int count;
		private final InnerConnection innerConnection;
		private final String host;
		private final int port;
		private final String method;
		private final HttpReadConnectHandler handler;
		private boolean redirected = false;
		
		public InnerRedirectReadHandler(int count, InnerConnection innerConnection, String host, int port, String method, HttpReadConnectHandler handler) {
			this.count = count;
			this.innerConnection = innerConnection;
			this.host = host;
			this.port = port;
			this.method = method;
			this.handler = handler;
		}
		
		@Override
		public void handle(int statusCode, String reason, Map<String, String> headers) {
			if (count > 0) {
				try {
					String location = headers.get(Http.LOCATION);
					if (location != null) {
						redirected = true;
						String newHost = host;
						int newPort = port;
						String newPath;
						if (location.startsWith(Http.PROTOCOL)) {
							String l = location.substring(Http.PROTOCOL.length());
							int i = l.indexOf(Http.PATH_SEPARATOR);
							if (i > 0) {
								newPath = l.substring(i);
								l = l.substring(0, i);
							} else {
								newPath = String.valueOf(Http.PATH_SEPARATOR);
							}
							int j = l.indexOf(Http.PORT_SEPARATOR);
							if (j < 0) {
								newPort = Http.DEFAULT_PORT;
								newHost = l;
							} else {
								String portValue = l.substring(j + 1);
								if (portValue.isEmpty()) {
									newPort = Http.DEFAULT_PORT;
								} else {
									try {
										newPort = Integer.parseInt(portValue);
									} catch (NumberFormatException e) {
										throw new IOException("Bad location: " + location);
									}
								}
								newHost = l.substring(0, j);
							}
						} else {
							newPath = location;
						}
		
						HttpConnection c = connect(newHost, newPort, method, newPath, null, new InnerRedirectReadHandler(count - 1, innerConnection, newHost, newPort, method, handler));
						innerConnection.set(c);
						redirected = true;
						return;
					}
				} catch (IOException e) {
					// ignored
				}
			}
			handler.handle(statusCode, reason, headers);
		}
		
		@Override
		public void handle(ByteBuffer buffer) {
			if (redirected) {
				return;
			}
			handler.handle(buffer);
		}
		
		@Override
		public void closed() {
			if (redirected) {
				return;
			}
			handler.closed();
		}
		
		@Override
		public void failed(IOException e) {
			if (redirected) {
				return;
			}
			handler.failed(e);
		}
	}
	
	private static final class InnerConnection implements HttpConnection {
		private final Worker worker;
		private boolean closed = false;
		private HttpConnection connection = null;
		public InnerConnection(Worker worker) {
			this.worker = worker;
		}
		@Override
		public void close() {
			worker.add(new SelectorVisitor() {
				@Override
				public void visit(Selector selector) {
					closed = true;
					connection.close();
					connection = null;
				}
			});
		}
		void set(HttpConnection connection) {
			if (closed) {
				connection.close();
			} else {
				this.connection = connection;
			}
		}
		@Override
		public void write(ByteBuffer buffer) {
			if (connection != null) {
				connection.write(buffer);
			}
		}
	}

	private static final class InnerReadHandler implements ReadHandler {
		private boolean recycled = false;
		private final HttpResponseReader reader;
		private final Map<String, List<RecyclingData>> recycling;
		private final String recyclingKey;
		private Connection connection;

		public InnerReadHandler(final Map<String, List<RecyclingData>> recycling, final String recyclingKey, final Connection connection, HttpReadConnectHandler handler) {
			reader = new HttpResponseReader(handler, new HttpResponseReader.RecyclingHandler() {
				@Override
				public void close() {
					connection.close();
				}
				@Override
				public void recycle() {
					synchronized (recycling) {
						List<RecyclingData> list = recycling.get(recyclingKey);
						if (list == null) {
							list = new LinkedList<RecyclingData>();
							recycling.put(recyclingKey, list);
						}
						list.add(new RecyclingData(connection));
						recycled = true;
					}
				}
			});
			this.recycling = recycling;
			this.recyclingKey = recyclingKey;
			this.connection = connection;
		}
		
		public boolean isRecycled() {
			synchronized (recycling) {
				return recycled;
			}
		}
		
		@Override
		public void handle(ByteBuffer buffer) {
			reader.handle(buffer);
		}
		@Override
		public void failed(IOException e) {
			reader.failed(e);
		}
		@Override
		public void closed() {
			synchronized (recycling) {
				if (recycled) {
					boolean found = false;
					List<RecyclingData> list = recycling.get(recyclingKey);
					if (list != null) {
						for (Iterator<RecyclingData> i = list.iterator(); i.hasNext();) {
							RecyclingData r = i.next();
							if (r.connection == connection) {
								i.remove();
								found = true;
								break;
							}
						}
					}
					assert(found);
					return;
				}
			}
			
			reader.closed();
		}
	}
	
	private static Map<String, String> prepareHeaders(String host, int port, Map<String, String> headers) {
		Map<String, String> h;
		if (headers == null) {
			h = new HashMap<String, String>();
		} else {
			h = new HashMap<String, String>(headers);
		}
		if (!h.containsKey(Http.HOST)) {
			h.put(Http.HOST, host + Http.PORT_SEPARATOR + port);
		}
		if (!h.containsKey(Http.ACCEPT_ENCODING)) {
			h.put(Http.ACCEPT_ENCODING, Http.GZIP);
		}
		return h;
	}
	
	public static ByteBuffer createRequest(String method, String host, int port, String path, Map<String, String> headers) {
		StringBuilder header = new StringBuilder();
		header.append(method).append(Http.START_LINE_SEPARATOR).append(path).append(Http.START_LINE_SEPARATOR).append(Http.HTTP11).append(Http.CR).append(Http.LF);
		
		headers = prepareHeaders(host, port, headers);
		
		for (Map.Entry<String, String> h : headers.entrySet()) {
			header.append(h.getKey()).append(Http.HEADER_KEY_VALUE_SEPARATOR).append(Http.HEADER_BEFORE_VALUE).append(h.getValue()).append(Http.CR).append(Http.LF);
		}
		header.append(Http.CR).append(Http.LF);
		return ByteBuffer.wrap(header.toString().getBytes(Http.USASCII_CHARSET));
	}
	
	private HttpConnection connect(String host, int port, String method, String path, Map<String, String> headers, final String recyclingKey, final Connection connection, final HttpReadConnectHandler handler) {
		final InnerReadHandler innerHandler = new InnerReadHandler(recycling, recyclingKey, connection, handler);

		connection.read(innerHandler);

		connection.write(createRequest(method, host, port, path, headers), new WriteHandler() {
			@Override
			public void failed(IOException e) {
				handler.failed(e);
			}
		});
		
		return new HttpConnection() {
			@Override
			public void close() {
				if (innerHandler.isRecycled()) {
					return;
				}
				connection.close();
			}
			@Override
			public void write(ByteBuffer buffer) {
				connection.write(buffer, new WriteHandler() {
					@Override
					public void failed(IOException e) {
						handler.failed(e);
					}
				});
			}
		};
	}
}
